package com.javaio.demo.nio.client;


import org.apache.logging.log4j.CloseableThreadContext;

import java.io.*;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.nio.charset.Charset;
import java.util.Objects;
import java.util.Set;

/**
 * 客户端主线程
 */
public class ChatClient {
    private final String TUICHU = "tuichu";
    private final String SERVER_IP = "127.0.0.1";
    private final int SERVER_PROT = 8888;
    /**
     * 缓冲区字节大小
     */
    private static final int BUFFER = 1024;

    /**
     * 同步阻塞服务端通道
     */
    private SocketChannel socketChannel;

    /**
     * 监控注册Channel(用来监控Channel 状态 是否是连接 或者读 或者取）
     */
    private Selector selector;
    /**
     * 缓冲区 因为nio通道Channle是往byteBuffer里面读取 bio是通过Steram流读取 这里采用的nio
     * ---处理读取的缓冲区
     */

    private ByteBuffer rBuffer = ByteBuffer.allocate(BUFFER);

    /**
     * 缓冲区 因为nio通道Channle是往byteBuffer里面读取 bio是通过Steram流读取 这里采用的nio
     * ---处理服务端连接和转发数据的缓冲区（SocketChannel）
     */
    private ByteBuffer wBuffer = ByteBuffer.allocate(BUFFER);

    /**
     * 编码解码 这里强制指定发送消息格式是UTF-8(避免乱码)
     */
    private Charset charset = Charset.forName("UTF-8");
    /**
     * 用来自定义用户自定义连接的端口
     */
    private int prot;

    /**
     * 用来自定义用户自定义连接的ip
     */
    private String host;

    public ChatClient(String host, int prot) {
        this.host = host;
        this.prot = prot;
    }

    public ChatClient() {
        this.host = SERVER_IP;
        this.prot = SERVER_PROT;
    }



    /**
     * 发送消息给服务器
     */
    public void send(String msg) throws IOException {
        //通过socket.shutdownInput()关闭输出流 只会关闭流
        //在客户端或者服务端通过socket.shutdownOutput()都是单向关闭的，
        // 即关闭客户端的输出流并不会关闭服务端的输出流，所以是一种单方向的关闭流；
        //判断客户端的输出流有没有关闭  单项关闭不会影响到服务端的socket
        if(!msg.isEmpty()){
              wBuffer.clear();
              wBuffer.put(charset.encode(msg));
              //读取转换
              wBuffer.flip();
              //如果缓冲区有消息 循环字节读取到客户端管道中
              while (wBuffer.hasRemaining()){
                  //读取消息进行像服务端传输
                  socketChannel.write(wBuffer);
              }
            //检查用户是否退出,因为该用户退出连接了
            if (readyTuiChu(msg)) {
               close(selector);
            }
        }
    }


    /**
     * 获取消息
     */
    public String receive(SocketChannel client) throws IOException {
        //清空该缓冲区 避免有旧数据污染
        rBuffer.clear();
        //因为是字节读取 要一个字节一个字节读取所以要循环  直到没有字节可读取退出  （这里要将客户端用户的数据循环写入到缓冲区）
        while (client.read(rBuffer) > 0) {
        }
        ;
        // 上面已经写入到了缓冲区这里要用函数flip进行读转换
        rBuffer.flip();
        //将字节转换
        return String.valueOf(charset.decode(rBuffer));
    }

    /**
     * 检查用户是否退出
     */
    public boolean readyTuiChu(String msg) {
        //这里还要判断用户发送的是否是退出消息如果是则关闭它的消息连接
        return TUICHU.equals(msg) ? true : false;
    }

    /**
     * 关闭该服务端
     */
    public synchronized void close(Closeable closeable) {
        //关闭没用的socket的资源 避免占用资源过多消耗
        if (!Objects.equals(null, closeable)) {
            try {
                //bufferedWriter关闭 socket也会关闭
                closeable.close();
                System.out.println("closeable");
            } catch (IOException e) {
                e.printStackTrace();
                System.out.println("该closeable资源关闭连接异常");
            }
        }
    }

    /**
     * 启动客户端
     */
    public void start() {
        try {
            //打开客户端通道
            socketChannel = SocketChannel.open();
            //设置非阻塞
            socketChannel.configureBlocking(false);
            //打开注册中心
            selector = Selector.open();
            //将客户端通道注册到注册中心用来进行监听事件 服务端监听ACCEPT 阻塞事件  客户端监听CONNECT事件获取服务端是否同意连接
            socketChannel.register(selector, SelectionKey.OP_CONNECT);
            //发起连接服务端请求
            socketChannel.connect(new InetSocketAddress(host, prot));
            System.out.println("客户端发起请求：" + prot + " -->正在监听用户事件");
            //不停监听selector获取事件
            while (true) {
                   //阻塞 (如果有这个事件触发 才会返回如果没有就会阻塞)因为阻塞就要不停的循环查找是否有事件
                    selector.select();
                    //得到当前事件的key  key有一组 不只是一个  有可能发生在多个通道上
                    Set<SelectionKey> selectionKeys = selector.selectedKeys();
                    //如果有事件就要处理当前事件 -- 迭代处理
                    for (SelectionKey selectionKey : selectionKeys) {
                        //处理当前事件
                        handles(selectionKey);
                    }
                    //如果处理完当前所以事件 要下一次重新处理新事件 也要从头开始(保证不能重复处理已经处理过的事件）
                    selectionKeys.clear();
            }
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClosedSelectorException s) {

        } finally {
        }
    }

    /**
     * 处理当前的selector事件(也就是处理服务端和客户端事件）
     *
     * @param key
     */
    public void handles(SelectionKey key) throws IOException {
        //如果触发了CONNECT事件 --客户端建立连接事件（服务端事件）
        if (key.isConnectable()) {
            //得到客户端通道
            SocketChannel serverChannel = (SocketChannel) key.channel();
            //如果返回true说明连接是就绪状态
            if (serverChannel.isConnectionPending()) {
                //正式建立连接
                serverChannel.finishConnect();
                //连接成功 开始进行主业务  用户输入逻辑  用户的输入是阻塞的 这是必须的，因为用户随时都有可能进行输入，不能让用户进行等待
                //副线程处理消息发送
                new Thread(new UserInputHandler(this)).start();
                //注册监控读事件  看有没有read事件过来  因为转发要注册这事件
                serverChannel.register(selector, SelectionKey.OP_READ);
            }
        }
        //如果是Read 事件 ---服务器转发事件（服务器事件）
        if (key.isReadable()) {
            //如果发现了读的事件获取当前事件的客户端
            SocketChannel client = (SocketChannel) key.channel();
            String fwdMsg = receive(client);
            //转发该服务器发送来的的消息
            if (fwdMsg.isEmpty()) {
                //如果没有消息那么不在监听该客户端了(因为和服务器出现了异常），服务器异常处理
                close(selector);
            } else {
                System.out.println(fwdMsg);
            }
        }
    }

    public static void main(String[] args) {
        ChatClient chatClient = new ChatClient();
        chatClient.start();
    }

}